package aconcagua.measure

class Measure extends MeasureBehavior implements Comparable {
	private amount;
	private unit;

	// (class) constructors
	public Measure(anAmount, aUnit) {
		super();
		amount = anAmount;
		unit = aUnit;
	}

	// accessing
	@Override
	public amount() {
		return amount;
	}

	public baseUnit() {
		return unit.baseUnit();
	}

	@Override
	public measures() {
		return this.isNothing() ? [] : [this];
	}

	@Override
	public unit() {
		return unit;
	}
	
	@Override
	public unitNameOf(aUnit) {
		return amount.abs() == 1 ? aUnit.nameForOne() : aUnit.nameForMany();
	}
	
	// arithmetic operations
	@Override
	public div(anArithmeticObject) {
		return anArithmeticObject.divideMeasure(this);
	}
	
	public divideFrom(aNumber) {
		return aNumber.isNothing() ? 0 : this.divideMeasure(aNumber);
	}
	
	@Override
	public intdiv(anArithmeticObject) {
		return anArithmeticObject.integerDivideMeasure(this);
	}
	
	public integerDivideFrom(aNumber) {
		return this.integerDivide(aNumber);
	}

	@Override
	public mod(anArithmeticObject) {
		return anArithmeticObject.moduloMeasure(this);
	}
	
	public moduloFrom(aNumber) {
		return this.modulo(aNumber);
	}

	@Override
	public multiply(anArithmeticObject) {
		return anArithmeticObject.multiplyMeasure(this);
	}

	@Override
	public negate() {
		return new Measure(-amount, unit);
	}

	@Override
	public plus(anArithmeticObject) {
		return anArithmeticObject.addMeasure(this);
	}
	
	@Override
	public plus(Infinity anInfinity) {
		return anInfinity;
	}
	
	@Override
	public minus(Infinity anInfinity) {
		return anInfinity.negate();
	}
	
	@Override
	public multiply(Infinity anInfinity) {
		return anInfinity;
	}

	@Override
	public round(aNumber) {
		return new Measure(amount.round(aNumber), unit);
	}

	@Override
	public trunc() {
		return new Measure(amount.trunc(), unit);
	}
	
	// arithmetic operations - private
	private addMeasure(aMeasure) {
		/* I delegate the responsibility to MeasureBag since this it verifies if the
		 * measures are of the same domain.
		 * When causing that bag has this responsibility, the code that checks this is
		 * in a single place and is not created bag for measures of the same domain.
		 */

		return MeasureBag.with(aMeasure, this);
	}
	
	private addMeasureBag(aMeasureBag) {
		return aMeasureBag.addMeasure(this);
	}

	public addSameDomainMeasure(aMeasure) {
		def measure;
		if(this.sameUnitAs(aMeasure))
			measure = new Measure(amount + aMeasure.amount(), unit);
		else
			measure = new Measure(this.convertAmountToBaseUnit() + aMeasure.convertAmountToBaseUnit(),
				this.baseUnit());
		return measure;
	}
	
	private divideMeasure(aMeasure) {
		def numerator = aMeasure.convertToBaseUnit();
		def denominator = this.convertToBaseUnit();
		return (numerator.unit() / denominator.unit()).with(numerator.amount() /
			denominator.amount());
	}
	
	private integerDivideMeasure(aMeasure) {
		def numerator = aMeasure.convertToBaseUnit();
		def denominator = this.convertToBaseUnit();
		return (numerator.unit() / denominator.unit()).with((numerator.amount() /
			denominator.amount()).toInteger());
	}
	
	private integerDivide(BigDecimal aNumber) {
		def denominator = this.convertToBaseUnit();
		return (aNumber.unit() / denominator.unit()).with((aNumber /
			denominator.amount()).toBigInteger());
	}
	
	private integerDivide(BigInteger aNumber) {
		def denominator = this.convertToBaseUnit();
		return (aNumber.unit() / denominator.unit()).with((aNumber /
			denominator.amount()).toBigInteger());
	}
	
	private integerDivide(aNumber) {
		return this.integerDivideMeasure(aNumber);
	}
	
	private moduloMeasure(aMeasure) {
		def numerator = aMeasure.convertToBaseUnit();
		def denominator = this.convertToBaseUnit();
		return numerator.baseUnit().with(numerator.amount() % denominator.amount());
	}
	
	private modulo(BigDecimal aNumber) {
		def denominator = this.convertToBaseUnit();
		return aNumber.baseUnit().with((aNumber % denominator.amount().toDouble()).toBigDecimal());
	}
	
	private modulo(aNumber) {
		return this.moduloMeasure(aNumber);
	}
	
	private modulo(BigInteger aNumber) {
		def denominator = this.convertToBaseUnit();
		return aNumber.baseUnit().with((aNumber % denominator.amount().toInteger()).toBigInteger());
	}
			
	private multiplyMeasure(aMeasure) {
		def leftFactor = aMeasure.convertToBaseUnit();
		def rightFactor = this.convertToBaseUnit();
		return (leftFactor.unit() * rightFactor.unit()).with(leftFactor.amount()
			* rightFactor.amount());
	}
	
	// bag creation - private
	private createBagWith(aMeasure) {
		return this.isNothing() ? aMeasure : aMeasure.createBagWithMeasure(this);
	}
	
	private createBagWithMeasure(aMeasure) {
		return this.isNothing() ? aMeasure : new MeasureBag([this, aMeasure]);
	}
	
	private createBagWithNumber(aNumber) {
		return this.isNothing() ? aNumber : new MeasureBag([this, aNumber]);
	}

	// comparing
	@Override
	public boolean equals(anObject) {
		return anObject.equalsMeasure(this);
	}
	
	@Override
	public int hashCode() {
		return this.convertAmountToBaseUnit().hashCode();
	}
	
	@Override
	public boolean equalsMeasure(aMeasure) {
		/* If the unit is the same one, verifies only the amount.
		 * If no, if it is of the same domain it turns the amount and it compares.
		 * If no, it returns false. */
	
		def result;
		if (this.sameUnitAs(aMeasure))
			result = amount == aMeasure.amount()
		else if (this.sameDomainAs(aMeasure))
			result = this.convertAmountToBaseUnit() == aMeasure.convertAmountToBaseUnit()
		else
			result = this.isNothing() && aMeasure.isNothing();
		return result;
	}
	
	@Override
	public boolean equalsMeasureBag(aMeasureBag) {
		return aMeasureBag.equalsMeasure(this);
	}

	private convertInSameDomainTo(aTargetUnit) {
		return aTargetUnit.convertFromBaseUnit(unit.convertToBaseUnit(this));
	}

	public convertTo(aTargetUnit) {
		if (!unit.sameDomainAs(aTargetUnit))
			throw new CanNotConvertMeasureException(this, aTargetUnit)
		else return this.convertInSameDomainTo(aTargetUnit);
	}

	@Override
	public convertToBaseUnit() {
		return this.convertTo(this.baseUnit());
	}
	
	// interval protocol
	public to(aStop) {
	 	return this.to(aStop, unit.with(1));
	}
	
	public to(aStop, aStep) {
		return new ArithmeticObjectInterval(this, aStop, aStep);
	}
	
	// magnitude protocol
	public between(aMeasure, anotherMeasure) {
		return this.between(InclusionStrategy.INCLUSIVE, aMeasure, anotherMeasure);
	}
	
	public between(anInclusionStrategy, aMeasure, anotherMeasure) {
		return anInclusionStrategy.value(this, aMeasure, anotherMeasure);
	}
	
	public max(aMeasure) {
		return (this < aMeasure) ? aMeasure : this;
	}
	
	public min(aMeasure) {
		return (this < aMeasure) ? this : aMeasure;
	}
	
	@Override
	public int compareTo(aMeasure) {
		def result = -1;
		if (aMeasure.equalsMeasure(this))
			result = 0;
		else if (aMeasure.sameDomainAs(this) || (aMeasure.baseUnit() == new NullUnit())) {
			if (aMeasure.greaterThanMeasure(this))
				result = -1;
			else if (aMeasure.lessThanMeasure(this))
				result = 1;
		}
		return result;
	}
	
	// magnitude protocol - private
	private greaterThanMeasure(aMeasure) {
		// If the unit is the same one, verifies only the amount.
		// aMeasure converts to baseUnit so that if there is some error of conversion
		// this jumps fast and generates CanNotConvertMeasureException correctly.
		
		def result;
		if (unit == aMeasure.unit())
			result = aMeasure.amount() < amount;
		else
			result = (aMeasure.convertTo(this.baseUnit())).amount() < this.convertAmountToBaseUnit();
		return result;
	}
	
	private lessThanMeasure(aMeasure) {
		// If the unit is the same one, verifies only the amount.
		// aMeasure converts to baseUnit so that if there is some error of conversion
		// this jumps fast and generates CanNotConvertMeasureException correctly.
		
		def result;
		if (unit == aMeasure.unit())
			result = aMeasure.amount() > amount;
		else
			result = (aMeasure.convertTo(this.baseUnit())).amount() > this.convertAmountToBaseUnit();
		return result;
	}
	
	// printing
	@Override
	public String toString() {
		return amount.toString() +" "+ unit.nameFor(amount);
	}
	
	// testing
	@Override
	public boolean isMeasureBag() {
		return false;
	}
		
	@Override
	public boolean isNothing() {
		return this.convertToBaseUnit().amount() == this.baseUnit().nothingAmount();
	}
	
	public boolean sameDomainAs(aMeasure) {
		return unit.sameDomainAs(aMeasure.unit());
	}
	
	public boolean sameUnitAs(aMeasure) {
		return unit == aMeasure.unit();
	}

}
